﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Xml;

namespace Quail
{
    /// <summary>
    /// Implements access to the TABLE html control
    /// </summary>
    public class WebTable : WebControl, IWebControlWithRows
    {
        private readonly Dictionary<int, RowData> _rowdata = new Dictionary<int, RowData>();
        private XmlDocument _xmlContentDom;

        /// <summary>
        /// Initializes a new instance of the <see cref="WebTable"/> class.
        /// </summary>
        /// <param name="container">The container (webpage or webcontrol)</param>
        /// <param name="selector">The jQuery selector</param>
        public WebTable(IQuailControl container, string selector)
            : base(container, selector)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WebTable"/> class.
        /// </summary>
        /// <param name="control">The control to copy</param>
        public WebTable(WebControl control)
            : base(control)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WebTable"/> class.
        /// </summary>
        public WebTable()
        {
        }

        /// <summary>
        /// Delegate for finding a matching row
        /// </summary>
        /// <param name="table">The table to test</param>
        /// <param name="row">The row to test</param>
        /// <returns>True if the row matches</returns>
        public delegate bool IsRowMatch(WebTable table, int row);

        /// <summary>
        /// Delegate for finding a matching column
        /// </summary>
        /// <param name="table">The table to test</param>
        /// <param name="row">The row to test</param>
        /// <param name="column">The column to test</param>
        /// <returns>True if the row matches</returns>
        public delegate bool IsColumnMatch(WebTable table, int row, int column);

        /// <summary>
        /// Gets a value indicating whether this instance's table content is cached
        /// <see cref="CacheTableContents"/>
        /// </summary>
        public bool IsTableContentCached
        {
            get
            {
                return _xmlContentDom != null;
            }
        }

        /// <summary>
        /// Gets the number of rows in the table
        /// </summary>
        public int RowCount
        {
            get
            {
                var task = BeginTask("RowCount");
                try
                {
                    int result;

                    if (_xmlContentDom != null)
                    {
                        result = _xmlContentDom.DocumentElement.SelectNodes("/Rs/R").Count;
                    }
                    else
                    {
                        var jq = "jQuery(this.browserbot.currentWindow.document).find('" + Selector + " tr').length";
                        result = int.Parse(Browser.GetEval(jq));
                    }

                    task.AddAttribute("RowCount", result.ToString());
                    task.Finish();

                    return result;
                }
                catch (Exception e)
                {
                    task.Finish(e);
                    throw;
                }
            }
        }

        /// <summary>
        /// Creates a selector to match a control within a given row and column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <returns>A selector to access the control within the given row/cell</returns>
        public string GetCellSelector(int row, int column)
        {
            return GetRowSelector(row) + " > " + GetColumnSelector(column);
        }


        /// <summary>
        /// Creates a selector to match a control within a given row and column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <param name="selector">The jQuery selector of the desired control</param>
        /// <returns>A selector to access the control within the given row/cell</returns>
        public string GetCellSelector(int row, int column, string selector)
        {
            var sel = GetCellSelector(row, column);
            if (string.IsNullOrEmpty(selector))
                return sel;
            return sel + " " + selector;
        }

        /// <summary>
        /// Gets the row selector.
        /// </summary>
        /// <param name="row">The row.</param>
        /// <returns>a jQuery selector for the row</returns>
        public string GetRowSelector(int row)
        {
            return "tr:eq(" + row + ")";
        }

        /// <summary>
        /// Gets the row selector for a nested control.
        /// </summary>
        /// <param name="row">The row.</param>
        /// <param name="selector">The selector.</param>
        /// <returns>A jQuery selector for the row</returns>
        public string GetRowSelector(int row, string selector)
        {
            return GetRowSelector(row) + " " + selector;
        }

        /// <summary>
        /// Gets the column selector.
        /// </summary>
        /// <param name="column">The column.</param>
        /// <returns>a jQuery selector for the column</returns>
        public string GetColumnSelector(int column)
        {
            return "td:eq(" + column + ")";
        }

        /// <summary>
        /// Gets a hashtable for the controls
        /// </summary>
        public Hashtable ControlHashtable
        {
            get
            {
                return HashControls;
            }
        }

        /// <summary>
        /// Gets the column selector for a nested control.
        /// </summary>
        /// <param name="column">The column.</param>
        /// <param name="selector">The selector.</param>
        /// <returns>A jQuery selector for the column</returns>
        public string GetColumnSelector(int column, string selector)
        {
            return GetColumnSelector(column) + " " + selector;
        }

        /// <summary>
        /// Gets the cell as a specific control
        /// </summary>
        /// <typeparam name="T">the type of control</typeparam>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <param name="selector">The jQuery selector of the desired control</param>
        /// <returns>A web control to access the control within the given row/cell</returns>
        public T GetCellAs<T>(int row, int column, string selector)
            where T : WebControl, new()
        {
            return Get<T>(GetCellSelector(row, column, selector));
        }

        /// <summary>
        /// Creates a WebControl object to match a specific row & column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <returns>A web control to access the control within the given row/cell</returns>
        public WebControl GetCellAsWebControl(int row, int column)
        {
            return GetCellAs<WebControl>(row, column, string.Empty);
        }

        /// <summary>
        /// Creates a WebControl object to match a control within a given row and column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <param name="selector">The jQuery selector of the desired control</param>
        /// <returns>A web control to access the control within the given row/cell</returns>
        public WebControl GetCellAsWebControl(int row, int column, string selector)
        {
            return GetCellAs<WebControl>(row, column, selector);
        }

        /// <summary>
        /// Creates a WebControl object to match a control within a given row and column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <param name="linktext">The text of the link to find</param>
        /// <returns>A web control to access the control within the given row/cell</returns>
        public WebLinkByText GetCellAsWebLinkByText(int row, int column, string linktext)
        {
            return GetCellAsWebControl(row, column).GetLinkByText(linktext);
        }

        /// <summary>
        /// Assumes a "square" table
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <returns>The number of columns for that row</returns>
        public int GetColumnCount(int row)
        {
            var task = BeginTask("GetColumnCount(" + row + ")");
            try
            {
                int result;

                if (_xmlContentDom != null)
                {
                    result = _xmlContentDom.DocumentElement.SelectNodes("/Rs/R[" + (row + 1) + "]/C").Count;
                }
                else
                {
                    var jq = "jQuery(this.browserbot.currentWindow.document).find('" + Selector + " tr:eq(" + row + ")').find('td,th').length";
                    result = int.Parse(Browser.GetEval(jq));
                }

                task.AddAttribute("Count", result.ToString());
                task.Finish();
                return result;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Gets the text of a particular row and column
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="column">The 0-based column number</param>
        /// <returns>The text of the cell</returns>
        public string GetCellText(int row, int column)
        {
            var task = BeginTask("GetCellText");
            task.AddAttribute("Row", row.ToString());
            task.AddAttribute("Column", column.ToString());
            try
            {
                string result = string.Empty;

                if (_xmlContentDom != null)
                {
                    var node = _xmlContentDom.DocumentElement.SelectSingleNode("/Rs/R[" + (row + 1) + "]/C[" + (column + 1) + "]");

                    if (node != null)
                    {
                        var attr = node.Attributes["text"];
                        if (attr != null)
                            result = attr.Value;
                    }
                }
                else
                {
                    result = Browser.SeleniumInterface.GetTable(Locator + "." + row + "." + column);
                }

                task.AddAttribute("Result", result);
                task.Finish();
                return result;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Returns the contents of the TABLE as a DOM (Text only)
        /// </summary>
        /// <returns>An Xml document</returns>
        public XmlDocument GetTableContentsAsXml()
        {
            var task = ActiveTask.BeginTask("GetTableContentsAsXml");
            try
            {
                var xml = Browser.DoCommand("getTableContentAsXML", Locator);
                var dom = new XmlDocument();
                dom.LoadXml(xml);

                task.Finish();
                return dom;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Searches a row of a table for a given text using a regular expression
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="text">The regular expression to find</param>
        /// <returns>The column number, or -1 if the text is not found</returns>
        public int GetColumnWithText(int row, Regex text)
        {
            var task = BeginTask("GetColumnWithText(" + row + ")");
            task.AddAttribute("Regex", text.ToString());

            try
            {
                // (WebTable table, int row) => text.IsMatch(table.GetCellText(row, column));
                int col = GetColumnThatMatches((table, r, c) => text.IsMatch(table.GetCellText(r, c)), row);
                task.AddAttribute("FoundColumn", col.ToString());
                task.Finish();
                return col;
            }
            catch (Exception e)
            {
                task.Finish(e);
                
                throw;
            }
        }

        /// <summary>
        /// Searches a row of a table for a given text
        /// </summary>
        /// <param name="row">The 0-based row number</param>
        /// <param name="text">The text to find</param>
        /// <returns>The column number, or -1 if the text is not found</returns>
        public int GetColumnWithText(int row, string text)
        {
            var colCount = GetColumnCount(row);

            for (var col = 0; col < colCount; col++)
            {
                var celltext = GetCellText(row, col);

                if (string.Compare(text, celltext, true) == 0)
                    return col;
            }

            return -1;
        }

        /// <summary>
        /// Searches the header row of the table for the header text
        /// </summary>
        /// <param name="headertext">The text to find</param>
        /// <returns>The column number, or -1 if the text is not found</returns>
        public int GetColumnIndex(string headertext)
        {
            return GetColumnWithText(0, headertext);
        }

        /// <summary>
        /// Searches the header row of the table for the header text using a regular expression
        /// </summary>
        /// <param name="headertext">The text to find</param>
        /// <returns>The column number, or -1 if the text is not found</returns>
        public int GetColumnIndex(Regex headertext)
        {
            return GetColumnWithText(0, headertext);
        }

        /// <summary>
        /// Searches all the rows and columns of a table for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text)
        {
            return GetRowWithCellText(text, -1, 0);
        }

        /// <summary>
        /// Searches all the rows of a table in the specified header colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="headercolumn">The text of the header column</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, string headercolumn)
        {
            return GetRowWithCellText(text, GetColumnIndex(headercolumn));
        }

        /// <summary>
        /// Searches all the rows of a table in the specified header colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="headercolumn">The text of the header column</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, Regex headercolumn)
        {
            return GetRowWithCellText(text, GetColumnIndex(headercolumn));
        }

        /// <summary>
        /// Searches all the rows of a table in the specified colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="column">The 0-based index of column</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, int column)
        {
            return GetRowWithCellText(text, column, 0);
        }

        /// <summary>
        /// Searches all the rows of a table in the specified colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="headercolumn">The text of the header column</param>
        /// <param name="startrow">The first row to search</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, string headercolumn, int startrow)
        {
            return GetRowWithCellText(text, GetColumnIndex(headercolumn), 0);
        }

        /// <summary>
        /// Searches all the rows of a table in the specified colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="headercolumn">The text of the header column</param>
        /// <param name="startrow">The first row to search</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, Regex headercolumn, int startrow)
        {
            return GetRowWithCellText(text, GetColumnIndex(headercolumn), 0);
        }

        /// <summary>
        /// Searches all the rows of a table in the specified colummn for text using a regular expression
        /// </summary>
        /// <param name="text">The text to find</param>
        /// <param name="column">The 0-based index of column</param>
        /// <param name="startrow">The first row to search</param>
        /// <returns>The first row found with a cell containing the text</returns>
        public int GetRowWithCellText(Regex text, int column, int startrow)
        {
            return GetRowThatMatches(GetRowMatchDelegate(text, column), startrow);
        }

        /// <summary>
        /// Creates a delegate for text/column search
        /// </summary>
        /// <param name="text">The text to search for.</param>
        /// <param name="column">The column.</param>
        /// <returns>A delegate</returns>
        public IsRowMatch GetRowMatchDelegate(Regex text, int column)
        {
            IsRowMatch rm;

            if (column >= 0)
                rm = (table, row) => text.IsMatch(table.GetCellText(row, column));
            else
                rm = (table, row) => table.GetColumnWithText(row, text) >= 0;
            return rm;
        }

        /// <summary>
        /// Looks for a row matching a IsRowMatch delegate
        /// </summary>
        /// <param name="matcher">the IsRowMatch delegate</param>
        /// <returns>-1 if no row was found, otherwise the number the row was found</returns>
        public int GetRowThatMatches(IsRowMatch matcher)
        {
            return GetRowThatMatches(matcher, 0);
        }

        /// <summary>
        /// Looks for a row matching a IsRowMatch delegate
        /// </summary>
        /// <param name="matcher">the IsRowMatch delegate</param>
        /// <param name="startrow">the row to start</param>
        /// <returns>-1 if no row was found, otherwise the number the row was found</returns>
        public int GetRowThatMatches(IsRowMatch matcher, int startrow)
        {
            var task = ActiveTask.BeginTask("GetRowThatMatches");
            task.AddAttribute("StartRow", startrow.ToString());
            try
            {
                if (!Exists)
                {
                    task.Finish(LogStatus.Pass);
                    return -1;
                }

                using (CacheTableContents())
                {
                    var maxRow = RowCount;
                    var foundrow = -1;
                    for (var row = startrow; row < maxRow; row++)
                    {
                        if (matcher(this, row))
                        {
                            foundrow = row;
                            break;
                        }
                    }

                    task.AddAttribute("Found", foundrow.ToString());
                    task.Finish();
                    return foundrow;
                }
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Caches the table contents - be sure to dipose the object
        /// </summary>
        /// <returns>A disposable object</returns>
        public IDisposable CacheTableContents()
        {
            return new TableContentsCache(this);
        }

        /// <summary>
        /// Looks for a column matching a IsColumnMatch delegate
        /// </summary>
        /// <param name="matcher">the IsRowMatch delegate</param>
        /// <param name="row">the row to search</param>
        /// <returns>-1 if no column was found, otherwise the number the column was found</returns>
        public int GetColumnThatMatches(IsColumnMatch matcher, int row)
        {
            var maxCol = GetColumnCount(row);
            for (var col = 0; col < maxCol; col++)
            {
                if (matcher(this, row, col))
                    return col;
            }

            return -1;
        }

        /// <summary>
        /// Implements a way to access row data using properties
        /// </summary>
        public class RowData
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="RowData"/> class.
            /// </summary>
            /// <param name="owner">The owner.</param>
            /// <param name="row">The row.</param>
            public RowData(WebTable owner, int row)
            {
                Table = owner;
                Row = row;
            }

            /// <summary>
            /// Gets the table.
            /// </summary>
            /// <value>The table.</value>
            public WebTable Table
            {
                get; private set;
            }

            /// <summary>
            /// Gets or sets the row.
            /// </summary>
            /// <value>The row.</value>
            public int Row
            {
                get; set;
            }

            /// <summary>
            /// Gets the cell selector.
            /// </summary>
            /// <param name="column">The column.</param>
            /// <param name="selector">The selector.</param>
            /// <returns></returns>
            public string GetCellSelector(int column, string selector)
            {
                return Table.GetCellSelector(Row, column, selector);
            }

            /// <summary>
            /// Gets the control.
            /// </summary>
            /// <param name="column">The column.</param>
            /// <param name="selector">The selector.</param>
            /// <returns></returns>
            public T Get<T>(int column, string selector)
                where T : WebControl, new()
            {
                return Table.GetCellAs<T>(Row, column, selector);
            }

            /// <summary>
            /// Gets the control matching a specific selector
            /// </summary>
            /// <param name="column">The column.</param>
            /// <param name="selector">The selector.</param>
            /// <returns></returns>
            public WebControl GetControl(int column, string selector)
            {
                return Table.GetCellAs<WebControl>(Row, column, selector);
            }

            /// <summary>
            /// Gets the link by text.
            /// </summary>
            /// <param name="column">The column.</param>
            /// <param name="text">The text.</param>
            /// <returns></returns>
            public WebLinkByText GetLinkByText(int column, string text)
            {
                return Table.GetCellAsWebLinkByText(Row, column, text);
            }


            /// <summary>
            /// Gets the text.
            /// </summary>
            /// <param name="column">The column.</param>
            /// <returns></returns>
            public string GetCellText(int column)
            {
                return Table.GetCellText(Row, column);
            }
        }

        /// <summary>
        /// Gets the row data.
        /// </summary>
        /// <param name="row">The row.</param>
        /// <returns></returns>
        public RowData GetRowData(int row)
        {
            RowData result;

            var rc = RowCount;
            if (row < 0 || row >= rc)
                throw new WebControlException(this, string.Format("Invalid row specified in call to GetRowData({0}).  RowCount={1}", row, rc));

            if (_rowdata.TryGetValue(row, out result))
                return result;

            result = CreateRowData(row);
            _rowdata.Add(row, result);

            return result;
        }

        /// <summary>
        /// Creates the row data instance.
        /// </summary>
        /// <param name="row">The row.</param>
        /// <returns></returns>
        public virtual RowData CreateRowData(int row)
        {
            return new RowData(this, row);
        }

        private class TableContentsCache : IDisposable
        {
            private readonly WebTable _table;
            public TableContentsCache(WebTable owner)
            {
                _table = owner;
                _table._xmlContentDom = _table.GetTableContentsAsXml();
            }

            public void Dispose()
            {
                _table._xmlContentDom = null;
            }
        }
    }
}
